/*
 * AgentComponent.java
 *
 * Created on April 22, 2007, 10:57 AM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package org.xeerkat.agent.tools;

import java.io.IOException;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import javax.naming.NamingException;
import org.infoset.xml.Document;
import org.infoset.xml.Element;
import org.infoset.xml.Name;
import org.infoset.xml.XMLException;
import org.restlet.Application;
import org.restlet.Component;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.Restlet;
import org.restlet.Server;
import org.restlet.Uniform;
import org.restlet.data.MediaType;
import org.restlet.data.Method;
import org.restlet.data.Protocol;
import org.restlet.data.Reference;
import org.restlet.data.Status;
import org.restlet.representation.Representation;
import org.restlet.representation.StringRepresentation;
import org.restlet.routing.VirtualHost;
import org.xeerkat.agent.AgentContext;
import org.xeerkat.agent.AgentException;
import org.xeerkat.agent.AgentMonitor;
import org.xeerkat.agent.GroupChat;
import org.xeerkat.agent.ResponseListener;
import org.xeerkat.agent.TaskManager;
import org.xeerkat.agent.services.Service;
import org.xeerkat.agent.services.ServiceRegistry;
import org.xeerkat.agent.services.ServiceRuntime;
import org.xeerkat.restlet.xmpp.XMPP;
import org.xeerkat.restlet.xmpp.XMPPException;
import org.xeerkat.xmpp.ID;

/**
 *
 * @author alex
 */
public class AgentComponent extends Component {

   static final ThreadGroup threadGroup = new ThreadGroup("xeerkat.agent");
   
   public interface ManagementListener {
       public void onManagement(ID to,ID from,String method,String username,String password,Document message);
   }
   
   static class ServiceApplication {
      Service service;
      Application app;
      ServiceApplication(Service service,Application app) {
         this.service = service;
         this.app = app;
      }
   }
   
   public class AgentEnvironment {
      
      class ComponentAgentMonitor extends AgentMonitor {
         ComponentAgentMonitor() {
            // monitor self for connection and reconnection
            identity.getSelfListeners().add(new XMPP.ResponseListener() {
               public void onResponse(UUID id, Response response) { }
               public void onOffline(ID id) {
                  for (AgentMonitor.Listener listener : getSelfListeners()) {
                     listener.onOffline(id);
                  }
               }
               public void onOnline(ID id) {
                  for (AgentMonitor.Listener listener : getSelfListeners()) {
                     listener.onOnline(id);
                  }
               }
               public void onManagement(ID from,String method,String username,String password,Document message) {
               }
            });
         }
         public boolean isConnected() {
            return identity.isConnected();
         }
         public void add(ID id) {
            known.add(id);
            for (Listener listener : getListeners()) {
               listener.onOnline(id);
            }
         }
         public void remove(ID id) {
            known.remove(id);
            for (Listener listener : getListeners()) {
               listener.onOffline(id);
            }
         }
   
         public void probePresence(ID to) 
            throws XMPPException
         {
            identity.sendProbe(to);
         }

         public void notifyManagement(ID to,String method,String username,String password,Document message) 
            throws XMPPException
         {
            identity.sendManagement(to,method, username, password, message);
         }

      }
      public class ComponentAgentContext implements AgentContext
      {
         Map<String,ResponseListener> responseListeners;

         /** Creates a new instance of AgentContext */
         public ComponentAgentContext()
         {
            responseListeners = new TreeMap<String,ResponseListener>();
         }

         void init() {
            identity.addResponseListener(new XMPP.ResponseListener() {
               public void onResponse(UUID id, Response response) 
               {
                  String sid = id.toString();
                  if (getLogger().isLoggable(Level.FINE)) {
                     getLogger().info("Response received for "+sid);
                  }
                  ResponseListener listener = responseListeners.remove(sid);
                  if (listener!=null) {
                     listener.onResponse(sid,response);
                  }
               }
               public void onManagement(ID from,String method,String username,String password,Document message)
               {
               }
               public void onOffline(ID id) {

               }
               public void onOnline(ID id) {

               }
            });
         }

         public boolean isOnline() {
            return identity.isConnected();
         }

         public boolean waitTillOnline(int time, TimeUnit unit) {
            if (isOnline()) {
               return true;
            }
            final CountDownLatch latch = new CountDownLatch(1);
            final AtomicReference<XMPP.ResponseListener> listener = new AtomicReference<XMPP.ResponseListener>();
            listener.set(new XMPP.ResponseListener() {

               public void onResponse(UUID id, Response response) {
               }

               public void onOffline(ID id) {
               }

               public void onOnline(ID id) {
                  identity.getSelfListeners().remove(listener.get());
                  latch.countDown();
               }

               public void onManagement(ID from, String method, String username, String password, Document message) {
               }
            });
            identity.getSelfListeners().add(listener.get());
            try {
               latch.await(time, unit);
            } catch (Exception ex) {}
            return isOnline();
         }
         public ID getIdentity() {
            ID id = identity.getConnectionId()==null ? identity.getId() : identity.getConnectionId();
            // TODO: optimize this.  The identity's ID has a password so we need to hide that.
            return new ID(id.getName(),id.getDomain(),id.getResource());
         }

         public Request createRequest(Method method,Reference resourceRef)
         {
            Request request = new Request(method,resourceRef);
            request.getAttributes().put("org.xeerkat.restlet.xmpp.from.id", getIdentity());
            return request;
         }

         public Reference createRequestReference(Protocol protocol,String to,String path) 
         {
            ID id = identity.isConnected() ? identity.getConnectionId() : identity.getId();
            return new Reference(protocol.getSchemeName()+"://"+id+"/"+to+path);
         }

         public AgentMonitor getAgentMonitor() {
            return agentMonitor;
         }

         public TaskManager getTaskManager() {
            return taskManager;
         }

         public Iterator<ServiceRuntime> getServices() {
            return registeredServices.iterator();
         }
         public void registerResponseListener(String id,ResponseListener listener) {
            responseListeners.put(id,listener);
         }

         public Iterator<GroupChat> getChats() {
            return rooms.values().iterator();
         }

         public GroupChat joinChat(ID chatId,GroupChat.Listener listener)
            throws AgentException
         {
            return AgentEnvironment.this.joinChat(chatId, null, listener);
         }

         public GroupChat joinChat(ID chatId,String password,GroupChat.Listener listener)
            throws AgentException
         {
            return AgentEnvironment.this.joinChat(chatId, password, listener);
         }

         public void registerStanzaTarget(String id,Uniform target) {
            addTarget(id,target);
         }
      }
      class AgentGroupChat implements GroupChat {
         ID room;
         ID id;
         String role;
         String affiliation;
         boolean inRoom;
         boolean isReady;
         GroupChat.Listener listener;
         Set<ID> members;
         Uniform defaultTarget;
         AgentGroupChat(ID room,ID id,GroupChat.Listener listener) {
            this.room = room;
            this.id = id;
            this.role = null;
            this.affiliation = null;
            this.inRoom = false;
            this.isReady = false;
            this.listener = listener;
            this.members = new TreeSet<ID>();
            this.defaultTarget = null;
         }

         public void join(final String password)
            throws AgentException
         {
            String msgId = UUID.randomUUID().toString();
            Uniform handler = new Uniform() {
               public void handle(Request request, Response response) {
                  response.setStatus(Status.SUCCESS_NO_CONTENT);
                  Element stanza = (Element)request.getAttributes().get("org.xeerkat.xmpp.stanza");
                  if ("error".equals(stanza.getAttributeValue("type"))) {
                     int code = -1;
                     String errorType = null;
                     Element errorE = stanza.getFirstElementNamed(org.xeerkat.xmpp.Protocol.ERROR);
                     if (errorE!=null) {
                        String codeS = errorE.getAttributeValue("code");
                        if (codeS!=null) {
                           code = Integer.parseInt(codeS);
                        }
                        Iterator<Element> errorTypes = errorE.getElementChildren();
                        if (errorTypes.hasNext()) {
                           errorType = errorTypes.next().getName().toString();
                        }
                     }
                     if (!("owner".equals(affiliation) && code==404)) {
                        if (listener!=null) {
                           listener.onJoinFailed(code,errorType);
                        }
                        getLogger().severe("Error from joining room "+room+": "+request.getEntityAsText());
                     }
                     return;
                  }
                  if (inRoom && isReady) {
                     return;
                  }
                  //getLogger().info(identity.getConnectionId()+" Response from conference server: "+request.getEntityAsText());
                  Element mucUserX = stanza.getFirstElementNamed(org.xeerkat.xmpp.Protocol.MUC_USER_X);
                  if (mucUserX==null) {
                     getLogger().severe("Missing MUC user response from server.");
                     return;
                  }
                  Element item = mucUserX.getFirstElementNamed(org.xeerkat.xmpp.Protocol.MUC_USER_ITEM);
                  role = item.getAttributeValue("role");
                  affiliation = item.getAttributeValue("affiliation");
                  Iterator<Element> statusCodes = mucUserX.getElementsByName(org.xeerkat.xmpp.Protocol.MUC_USER_STATUS);
                  boolean created = false;
                  inRoom = false;
                  isReady = false;
                  while (statusCodes.hasNext()) {
                     String codeS = statusCodes.next().getAttributeValue("code");
                     if (codeS!=null) {
                        if (Integer.parseInt(codeS)==110) {
                           inRoom = true;
                           isReady = true;
                        }
                        if (Integer.parseInt(codeS)==201) {
                           created = true;
                           inRoom = true;
                        }
                     }
                  }
                  if (created) {
                     // we need to configure the room
                     try {
                        configure(password);
                     } catch (Exception ex) {
                        getLogger().severe(ex.getMessage());
                     }
                  }
                  if (listener!=null && inRoom) {
                     members.add(id);
                     listener.onJoin(id);
                  }
                  if (listener!=null && isReady) {
                     listener.onReady();
                  }
               }
            };
            addTarget(msgId,handler);
            addFromTarget(id.toString(),handler);

            String xml = "<presence xmlns='jabber:client' to='"+id+"' id='"+msgId+"'>"+(password==null ? "<x xmlns='http://jabber.org/protocol/muc'/>" : "<x xmlns='http://jabber.org/protocol/muc'><password>"+password+"</password></x>")+"</presence>";
            Request request = new Request(Method.POST,new Reference("xmpp://"+identity.getConnectionId()));
            request.setEntity(new StringRepresentation(xml,MediaType.APPLICATION_XML));

            Response response = getContext().getClientDispatcher().handle(request);
            if (!response.getStatus().isSuccess()) {
               throw new AgentException("Cannot join due to transport error: "+response.getEntityAsText());
            }
         }

         void configure(String password)
            throws AgentException
         {
            String msgId = UUID.randomUUID().toString();
            addTarget(msgId, new Uniform() {
               public void handle(Request request, Response response) {
                  response.setStatus(Status.SUCCESS_NO_CONTENT);
                  Element stanza = (Element)request.getAttributes().get("org.xeerkat.xmpp.stanza");
                  if ("error".equals(stanza.getAttributeValue("type"))) {
                     Element errorE = stanza.getFirstElementNamed(org.xeerkat.xmpp.Protocol.ERROR);
                     int code = -1;
                     String errorType = null;
                     if (errorE!=null) {
                        String codeS = errorE.getAttributeValue("code");
                        if (codeS!=null) {
                           if (codeS.trim().equals("405")) {
                              // instant room cannot be created, try getting configuration
                              Iterator<Element> errorTypes = errorE.getElementChildren();
                              if (errorTypes.hasNext()) {
                                 errorType = errorTypes.next().getName().toString();
                              }
                           }
                           code = Integer.parseInt(codeS);
                        }
                     }
                     getLogger().severe("Cannot configure room: "+request.getEntityAsText());
                     isReady = false;
                     if (listener!=null) {
                        listener.onCreateFailed(code, errorType);
                     }
                  } else if ("result".equals(stanza.getAttributeValue("type"))) {
                     isReady = true;
                     if (listener!=null && isReady) {
                        listener.onReady();
                     }
                  }
               }
            });
            StringBuilder xml = new StringBuilder();
            xml.append("<iq xmlns='jabber:client' type='set' to='");
            xml.append(room.toString());
            xml.append("' id='");
            xml.append(msgId);
            xml.append("'><query xmlns='http://jabber.org/protocol/muc#owner'>");
            if (password==null) {
               xml.append("<x xmlns='jabber:x:data' type='submit'/>");
            } else {
               xml.append("<x xmlns='jabber:x:data' type='submit'>");
               xml.append("<field var='FORM_TYPE'><value>http://jabber.org/protocol/muc#roomconfig</value></field>");
               xml.append("<field var='muc#roomconfig_roomsecret'><value>");
               xml.append(password);
               xml.append("</value></field></x>");
            }
            xml.append("</query></iq>");
            Request request = new Request(Method.POST,new Reference("xmpp://"+identity.getConnectionId()));
            request.setEntity(new StringRepresentation(xml.toString(),MediaType.APPLICATION_XML));

            Response response = getContext().getClientDispatcher().handle(request);
            if (!response.getStatus().isSuccess()) {
               throw new AgentException("Cannot configure room due to transport error: "+response.getEntityAsText());
            }
         }

         public ID getRoom() {
            return room;
         }

         public ID getId() {
            return id;
         }

         public String getRole() {
            return role;
         }

         public String getAffiliation() {
            return affiliation;
         }

         public boolean isMember() {
            return inRoom;
         }

         public Iterator<ID> getMembers() {
            return members.iterator();
         }

         public void leave() {
            if (identity.isConnected()) {
               getLogger().fine("Sending unavailable to "+id);
               String xml = "<presence xmlns='jabber:client' to='"+id+"' type='unavailable'/>";
               Request request = new Request(Method.POST,new Reference("xmpp://"+identity.getConnectionId()));
               request.setEntity(new StringRepresentation(xml,MediaType.APPLICATION_XML));

               Response response = getContext().getClientDispatcher().handle(request);
               if (!response.getStatus().isSuccess()) {
                  getLogger().severe("Cannot leave group chat due to transport error: "+response.getEntityAsText());
               }
            } else {
               getLogger().fine("Identity "+identity.getId()+" is not connected, not necessary to leave chat "+room);
            }
            AgentEnvironment.this.leaveChat(room);
         }

         public void invite(ID id,String message) {

         }

         public void registerTarget(String id,Uniform target) {
            AgentEnvironment.this.addTarget(id, target);
         }

         public void setDefaultTarget(Uniform target) {
            defaultTarget = target;
         }

      }
      List<ServiceRegistry> registries;
      List<ServiceRuntime> registeredServices;
      List<ID> brokers;
      XMPP.Identity identity;
      Map<String,ServiceApplication> applications;
      AtomicBoolean brokerNotifyRunning;
      Thread brokerNotifyThread;
      ComponentAgentMonitor agentMonitor;
      ComponentAgentContext agentContext;
      TaskManager taskManager;
      List<VirtualHost> hosts;
      boolean started = false;
      Map<String,Object> attributes;
      Map<String,GroupChat> rooms;
      Map<String,Uniform> waitingTargets;
      Map<String,Uniform> fromRoutes;
      
      AgentEnvironment(XMPP xmpp,Configuration.Agent agent)
         throws NamingException,UnknownHostException
      {
         this.identity = xmpp.addIdentity(agent.getId());
         this.brokers = new ArrayList<ID>();
         this.brokers.addAll(agent.getBrokers());
         this.registries = new ArrayList<ServiceRegistry>();
         this.registries.addAll(agent.getServiceRegistries());
         this.registeredServices = new ArrayList<ServiceRuntime>();
         this.applications = new TreeMap<String,ServiceApplication>();
         this.taskManager = new TaskManager(getLogger());
         this.agentContext = new ComponentAgentContext();
         this.agentMonitor = new ComponentAgentMonitor();
         this.hosts = new ArrayList<VirtualHost>();
         this.attributes = new TreeMap<String,Object>();
         this.rooms = new TreeMap<String,GroupChat>();
         this.waitingTargets = new TreeMap<String,Uniform>();
         this.fromRoutes = new TreeMap<String,Uniform>();
         
         attributes.put(AgentContext.AGENT_CONTEXT_ATTR,agentContext);

         agentContext.init();

         if (agent.allowBuiltin()) {
            URL servicesLoc = getClass().getResource("/org/xeerkat/agent/apps/services.xml");
            if (servicesLoc!=null) {
               try {
                  URI uri = servicesLoc.toURI();
                  ServiceRegistry builtin = new ServiceRegistry(uri.toString(),uri);
                  builtin.load();
                  registries.add(builtin);
               } catch (URISyntaxException ex) {
                  // shouldn't happen
                  getLogger().log(Level.SEVERE,"Cannot convert URL "+servicesLoc+" to URI.",ex);
               } catch (IOException ex) {
                  getLogger().log(Level.SEVERE,"I/O error while reading "+servicesLoc,ex);
               } catch (XMLException ex) {
                  getLogger().log(Level.SEVERE,"XML error while reading "+servicesLoc,ex);
               }
            }
         }

         getServers().add(XMPP.XMPP_PROTOCOL, identity.getId().toString(),0).getContext().getAttributes().put(XMPP.XMPP_ATTR,xmpp);


         identity.addResponseListener(new XMPP.ResponseListener() {
            public void onOffline(ID id)
            {
               if (getLogger().isLoggable(Level.FINE)) {
                  getLogger().info(id+" is offline for "+identity.getId());
               }
               agentMonitor.remove(id);
            }
            public void onOnline(ID id)
            {
               if (getLogger().isLoggable(Level.FINE)) {
                  getLogger().info(id+" is online for "+identity.getId());
               }
               agentMonitor.add(id);
            }
            public void onResponse(UUID id,Response response) {
               if (getLogger().isLoggable(Level.FINE)) {
                  StringBuilder builder = new StringBuilder();
                  builder.append("ID: "+id+"\n");
                  builder.append("Status: "+response.getStatus().getCode()+"\n");
                  Representation rep = response.getEntity();
                  if (rep!=null) {
                     builder.append("Content-Type: "+rep.getMediaType()+"\n");
                     try {
                        builder.append(rep.getText());
                     } catch (IOException ex) {
                        ex.printStackTrace();
                     }
                  }
                  getLogger().info(builder.toString());
               }
            }
            public void onManagement(ID from,String method,String username,String password,Document message) {
               notifyManagement(identity.getId(),from,method,username,password,message);
            }
         });

         for (Configuration.Host host : agent.getHosts()) {
            VirtualHost vhost = null;
            for (VirtualHost defined : getHosts()) {
               if (defined.getHostDomain().equals(host.getName())) {
                  boolean match = true;
                  // check ports
                  if (host.getAddress()!=null && defined.getServerAddress()!=null) {
                     match = host.getAddress().equals(defined.getServerAddress());
                  }
                  if (match && ((defined.getHostPort()!=null && host.getPort()<0) || (defined.getHostPort()==null && host.getPort()>0) ||
                               (!defined.getHostPort().equals(Integer.toString(host.getPort()))) ) ){
                     match = false;
                  }
                  if (match) {
                     vhost = defined;
                     break;
                  }
               }
            }
            if (vhost==null) {
               vhost = bindTransport(host.getName(),host.getAddress(),host.getPort());
            }
            this.hosts.add(vhost);
         }
         VirtualHost xmppHost = bindTransport(identity.getHost());
         this.hosts.add(xmppHost);
         final String forwardTarget = agent.getForwardTarget();
         getInternalRouter().attach("/xmpp/"+identity.getId(),new Restlet(getContext().createChildContext()) {
            public void handle(Request request, Response response) {
               boolean isFineLog = getLogger().isLoggable(Level.FINE);
               Element stanza = (Element)request.getAttributes().get("org.xeerkat.xmpp.stanza");
               if (stanza!=null) {
                  String id = stanza.getAttributeValue("id");
                  if (id!=null) {
                     Uniform uniform = getTarget(id);
                     if (uniform!=null) {
                        if (isFineLog) {
                           getLogger().fine("Routing message with id "+id+" to handler.");
                        }
                        uniform.handle(request, response);
                        return;
                     }
                  }
                  String from = stanza.getAttributeValue("from");
                  if (from!=null) {
                     Uniform uniform = getFromTarget(from);
                     if (uniform!=null) {
                        uniform.handle(request, response);
                        return;
                     }
                     Set<String> names = new TreeSet<String>();
                     synchronized (rooms) {
                        names.addAll(rooms.keySet());
                     }
                     for (String name : names) {
                        if (from.startsWith(name)) {
                           if (isFineLog) {
                              getLogger().fine("Message from room "+name);
                           }
                           AgentGroupChat chat = (AgentGroupChat)rooms.get(name);
                           if (stanza.getName().equals(org.xeerkat.xmpp.Protocol.PRESENCE)) {
                              // room presence
                              ID fromId = new ID(from);
                              String type = stanza.getAttributeValue("type");
                              if ("unavailable".equals(type)) {
                                 chat.members.remove(fromId);
                                 if (chat.listener!=null) {
                                    chat.listener.onLeave(fromId);
                                 }
                              } else {
                                 if (isFineLog) {
                                    getLogger().fine("Presence: "+fromId+", "+type);
                                 }
                                 chat.members.add(fromId);
                                 if (chat.listener!=null) {
                                    chat.listener.onJoin(fromId);
                                 }
                              }
                              return;
                           } else if (chat.defaultTarget!=null) {
                              chat.defaultTarget.handle(request, response);
                              return;
                           } else {
                              break;
                           }
                        }
                     }
                     if (isFineLog) {
                        getLogger().fine("No from route selected for "+from);
                     }
                  }
               }
               if (forwardTarget!=null) {
                  Reference forwardRef = new Reference("riap://component/agent/"+identity.getConnectionId()+forwardTarget);
                  if (isFineLog) {
                     getLogger().fine("Forwarding message to "+forwardRef);
                  }
                  Request forwarded = new Request(request.getMethod(),forwardRef);
                  forwarded.setAttributes(request.getAttributes());
                  forwarded.setEntity(request.getEntity());
                  getContext().getClientDispatcher().handle(forwarded, response);
               } else {
                  Name name = (Name)request.getAttributes().get("org.xeerkat.xmpp.type");
                  if (!name.equals(org.xeerkat.xmpp.Protocol.PRESENCE)) {
                     try {
                        getLogger().info("Unable to process message: "+request.getEntity().getText());
                     } catch (IOException ex) {
                        getLogger().log(Level.SEVERE,"I/O error during log of message.",ex);
                     }
                  }
                  response.setStatus(Status.SUCCESS_NO_CONTENT);
               }
            }
         });

      }

      void addFromTarget(String from,Uniform target) {
         synchronized (fromRoutes) {
            fromRoutes.put(from, target);
         }
      }

      Uniform getFromTarget(String from) {
         Uniform uniform = fromRoutes.get(from);
         if (uniform!=null) {
            synchronized (fromRoutes) {
               fromRoutes.remove(from);
            }
         }
         return uniform;
      }

      void addTarget(String id,Uniform target) {
         synchronized (waitingTargets) {
            waitingTargets.put(id, target);
         }
      }

      Uniform getTarget(String id) {
         Uniform uniform = waitingTargets.get(id);
         if (uniform!=null) {
            synchronized (waitingTargets) {
               waitingTargets.remove(id);
            }
         }
         return uniform;
      }

      GroupChat joinChat(ID chatId,String password,GroupChat.Listener listener) 
         throws AgentException
      {
         ID room = new ID(chatId.getName(),chatId.getDomain());

         getLogger().fine("Joining room "+room);
         AgentGroupChat chat = new AgentGroupChat(room,chatId,listener);
         synchronized (rooms) {
            rooms.put(room.toString(),chat);
         }
         chat.join(password);
         return chat;
      }

      void leaveChat(ID room) {
         synchronized (rooms) {
            rooms.remove(room.toString());
         }
      }
      
      void load() {
         /*
         applications.put("/status",new StatusApplication());
         applications.put("/services",new ServicesApplication());
          */
         for (ServiceRegistry registry : registries) {
            for (Service service : registry.values()) {
               if (globalApps.get(service.getPath())!=null) {
                  getContext().getLogger().warning("A global application already exists at path "+service.getPath()+", ignored application "+service.getName()+" for interface "+service.getInterface());
                  continue;
               }
               if (applications.get(service.getPath())!=null) {
                  getContext().getLogger().warning("An application already exists at path "+service.getPath()+", ignored application "+service.getName()+" for interface "+service.getInterface());
                  continue;
               }
               try {
                  service.init();
                  getLogger().info("Adding service "+service.getName()+","+service.getInterface()+","+service.getMetadata()+" at path "+service.getPath());
                  loadService(service);
               } catch (Exception ex) {
                  getContext().getLogger().log(Level.SEVERE,"Cannot load application "+service.getName()+" for interface "+service.getInterface()+" from location "+service.getLocation(),ex);
               }
            }
         }
      }
      
      public void loadService(Service service)
         throws Exception
      {
         Context appContext = getContext().createChildContext();
         for (String key : attributes.keySet()) {
            Object value = attributes.get(key);
            getLogger().info("Attribute: "+key+" -> "+value);
            appContext.getAttributes().put(key, value);
         }
         Application app = service.newApplication(appContext);
         if (app==null) {
            getContext().getLogger().severe("Cannot load application "+service.getName()+" for interface "+service.getInterface()+" from location "+service.getLocation());
         }
         app.getTunnelService().setEnabled(false);
         app.getTunnelService().setExtensionsTunnel(false);
         app.getTunnelService().setPreferencesTunnel(false);
      
         ServiceApplication sapp = new ServiceApplication(service,app);
         applications.put(service.getPath(),sapp);
         if (started) {
            String path = service.getPath();
            getLogger().info("Attaching application /service"+app+" at path "+path);
            getInternalRouter().attach("/service"+path,app);
            for (VirtualHost host : hosts) {
               getLogger().info("Attaching application "+app+" at path "+path+" to host "+host.getHostDomain()+":"+host.getHostPort());
               host.attach(path,app);
            }
         }
         registeredServices.add(new ServiceRuntime(service.getMetadata(),service.getPath()));
      }
      
      public void unloadService(String path,URI iface)
         throws Exception
      {
         ServiceApplication sapp = applications.remove(path);
         if (sapp!=null) {
            if (iface!=null && !sapp.service.getInterface().equals(iface)) {
               getLogger().warning("Unloading non-matching service "+sapp.service.getInterface()+" for path "+path+" and interface "+iface);
            }
            for (VirtualHost host : hosts) {
               getLogger().info("Detaching path "+path+" from host "+host.getHostDomain()+":"+host.getHostPort());
               host.detach(sapp.app);
            }
            List<ServiceRuntime> toRemove = new ArrayList<ServiceRuntime>();
            for (ServiceRuntime runtime : registeredServices) {
               if (sapp.service.getMetadata()==runtime.getMetadata()) {
                  toRemove.add(runtime);
               }
            }
            for (ServiceRuntime runtime : toRemove) {
               registeredServices.remove(runtime);
            }
         }
      }
      
      void attach() {
         for (String path : applications.keySet()) {
            ServiceApplication sapp = applications.get(path);
            getLogger().info("Attaching application "+sapp.app+" at path /service"+path);
            getInternalRouter().attach("/service"+path,sapp.app);
            for (VirtualHost host : hosts) {
               getLogger().info("Attaching application "+sapp.app+" at path "+path+" to host "+host.getHostDomain()+":"+host.getHostPort());
               host.attach(path,sapp.app);
            }
            String agentPath = "/agent/"+identity.getId()+"/{resource}/service"+path;
            getLogger().info("Attaching application "+sapp.app+" at path "+agentPath);
            getInternalRouter().attach(agentPath,sapp.app);
         }
      }

      public void start() {
         started = true;
         brokerNotifyRunning = new AtomicBoolean(true);
         brokerNotifyThread = new Thread(threadGroup,new Runnable() {
            public void run() {

               while (brokerNotifyRunning.get()) {
                  boolean reconnect = false;
                  for (ID id : brokers) {
                     try {
                        getLogger().info("Notifying "+id+" that agent is online.");
                        identity.sendOnline(id);
                     } catch (XMPPException ex) {
                        getLogger().log(Level.SEVERE,"Cannot send presence to "+id,ex);
                        reconnect = true;
                        break;
                     }
                  }
                  if (reconnect) {
                     try {
                        identity.reconnect();
                     } catch (XMPPException ex) {
                        getLogger().log(Level.SEVERE,"Reconnect after I/O error failed.",ex);
                     }
                  }
                  try {
                     Thread.currentThread().sleep(60*1000);
                  } catch (InterruptedException ex) {

                  }
               }
            }
         },"broker-notify "+identity.getId());
         brokerNotifyThread.start();

         final AtomicReference<XMPP.ResponseListener> listener = new AtomicReference<XMPP.ResponseListener>();
         listener.set(new XMPP.ResponseListener() {
            public void onResponse(UUID id, Response response) {
            }

            public void onOffline(ID id) {
            }

            public void onOnline(ID id) {
               // Auto start all service applications
               for (ServiceApplication sapp : applications.values()) {
                  if (!sapp.app.isStarted()) {
                     try {
                        sapp.app.start();
                     } catch (Exception ex) {
                        getLogger().log(Level.SEVERE,"Error starting service application.",ex);
                     }
                  }
               }
               identity.getSelfListeners().remove(listener.get());
            }

            public void onManagement(ID from, String method, String username, String password, Document message) {
            }
         });
         identity.getSelfListeners().add(listener.get());
      }

      public void stop() 
         throws Exception
      {
         started = false;
         getLogger().info("Stopping tasks for "+identity.getId());
         taskManager.stop();
         getLogger().info("Disconnecting from "+identity.getId());
         identity.disconnect();
         if (brokerNotifyRunning!=null && brokerNotifyRunning.get()) {
            getLogger().info("Shutting down broker for "+identity.getId());
            brokerNotifyRunning.set(false);
            try {
               synchronized (brokerNotifyThread) {
                  brokerNotifyThread.interrupt();
               }
               brokerNotifyThread.join();
            } catch (InterruptedException ex) {

            }
         }
      }
   }

   Configuration conf;
   List<ServiceRegistry> globalServices;
   Map<String,Application> globalApps;
   Map<ID,AgentEnvironment> agents;
   List<ManagementListener> managementListeners;
   Set<String> allowDomains;
   Set<ID> allowFrom;
   XMPP xmpp;
   
   /** Creates a new instance of AgentComponent */
   public AgentComponent(Configuration conf)
   {
      this.xmpp = new XMPP();
      this.conf = conf;
      try {
         URI location = this.conf.getLocation();
         if (location!=null) {
            location.toURL().openConnection().setDefaultUseCaches(false);
         }
      } catch (Exception ex) {
         getLogger().severe("Exception while disabling caching: "+ex.getMessage());
      }
      this.allowDomains = conf.getAllowArchiveFrom();
      this.allowFrom = conf.getUpgradesAllowedFrom();
      this.agents = new TreeMap<ID,AgentEnvironment>();
      this.managementListeners = new ArrayList<ManagementListener>();
      if (conf.getKeyStorePath()!=null) {
         getContext().getParameters().add("keystorePath",conf.getKeyStorePath().getAbsolutePath());
         getContext().getParameters().add("keystorePassword",conf.getKeyStorePassword());
         getContext().getParameters().add("keyPassword",conf.getKeyStorePassword());
      }
      for (Configuration.Interface iface : conf.getInterfaces()) {
         Server server = addTransport(iface.isSecure() ? Protocol.HTTPS : Protocol.HTTP,iface.getAddress().equals("*") ? null : iface.getAddress(), iface.getPort());
         if (iface.isSecure() && conf.getKeyStorePath()!=null) {
            server.getContext().getParameters().add("keystorePath",conf.getKeyStorePath().getAbsolutePath());
            server.getContext().getParameters().add("keystorePassword",conf.getKeyStorePassword());
            server.getContext().getParameters().add("keyPassword",conf.getKeyStorePassword());
         }
      }
      /*
      for (Configuration.Host host : conf.getHosts()) {
         bindTransport(host.getName(),host.getAddress(),host.getPort());
      }
      getServiceRegistries().addAll(conf.getServiceRegistries());
      agent.getBrokers().addAll(conf.getBrokers());
       */
      
      getLogService().setLoggerName("org.xeerkat.www");
      
      // ------------------
      // Add the connectors
      // ------------------
      getClients().add(XMPP.XEERKAT_PROTOCOL).getContext().getAttributes().put(XMPP.XMPP_ATTR,xmpp);
      getClients().add(XMPP.XEERKAT_SYNC_PROTOCOL).getContext().getAttributes().put(XMPP.XMPP_ATTR,xmpp);
      getClients().add(XMPP.XMPP_PROTOCOL).getContext().getAttributes().put(XMPP.XMPP_ATTR,xmpp);
      getClients().add(Protocol.HTTP);
      getClients().add(Protocol.HTTPS);
      getClients().add(Protocol.FILE);

      for (Configuration.Agent agent : conf.getAgents().values()) {
         try {
            agents.put(agent.getId(),new AgentEnvironment(xmpp,agent));
         } catch (NamingException ex) {
            getLogger().log(Level.SEVERE,"Cannot get XMPP domain name for "+agent.getId(),ex);
         } catch (UnknownHostException ex) {
            getLogger().log(Level.SEVERE,"Cannot resolve domain name.",ex);
         }
      }
      
      globalServices = conf.getServiceRegistries();
      this.globalApps = new TreeMap<String,Application>();
      
      for (Server server : getServers()) {
         try {
            server.start();
         } catch (Exception ex) {
            getLogger().log(Level.SEVERE,"Cannot start server.",ex);
         }
      }
      
   }
   
   public Configuration getConfiguration() {
      return conf;
   }
   
   public List<ManagementListener> getUpgradeListeners() {
      return managementListeners;
   }
   
   protected void notifyManagement(ID to,ID from,String method,String username,String password,Document message)
   {
      for (ManagementListener listener : managementListeners) {
         listener.onManagement(to, from, method,username, password, message);
      }
   }
   
   public void start() 
      throws Exception
   {
      super.start();

      getLogger().info("Loading global applications...");
      
      for (ServiceRegistry registry : globalServices) {
         for (Service service : registry.values()) {
            if (globalApps.get(service.getPath())!=null) {
               getContext().getLogger().warning("An application already exists at path "+service.getPath()+", ignored application "+service.getName()+" for interface "+service.getInterface());
               continue;
            }
            try {
               service.init();
               getLogger().info("Adding service "+service.getName()+","+service.getInterface()+" at path "+service.getPath());
               Context appContext = new Context(getLogger());
               appContext.getAttributes().putAll(getContext().getAttributes());
               Application app = service.newApplication(appContext);
               if (app==null) {
                  getContext().getLogger().severe("Cannot load application "+service.getName()+" for interface "+service.getInterface()+" from location "+service.getLocation());
               }
               globalApps.put(service.getPath(),app);
            } catch (Exception ex) {
               getContext().getLogger().log(Level.SEVERE,"Cannot load application "+service.getName()+" for interface "+service.getInterface()+" from location "+service.getLocation(),ex);
            }
         }
      }
      for (String path : globalApps.keySet()) {
         Application app = globalApps.get(path);
         getLogger().info("Attaching application "+app+" at path /service"+path);
         getInternalRouter().attach("/service"+path,app);
         for (VirtualHost host : getHosts()) {
            getLogger().info("Attaching application "+app+" at path "+path+" to host "+host.getHostDomain()+":"+host.getHostPort());
            host.attach(path,app);
         }
      }
      getLogger().info("Loading agent applications...");
      for (AgentEnvironment env : agents.values()) {
         env.load();
         env.attach();
         env.start();
      }
   }
   
   public AgentEnvironment getAgentEnvironment(ID id)
   {
      return agents.get(id);
   }
   
   public void bindAttribute(String key, Object value) {
      getContext().getAttributes().put(key, value);
      for (AgentEnvironment env : agents.values()) {
         env.attributes.put(key, value);
      }
   }
   
   public void stop()
      throws Exception
   {
      getLogger().info("Agent stopping.");
      super.stop();
      getLogger().info("Stopping environments:");
      for (AgentEnvironment env : agents.values()) {
         getLogger().info("Environment: "+env.identity.getId());
         env.stop();
      }
      getLogger().info("Agent stopped.");
   }
   
   public Server addTransport(Protocol protocol,String address, int port)
   {
      getLogger().info("https listening on "+protocol+" "+address+" "+port);
      return getServers().add(protocol, address, port);
   }
   
   protected VirtualHost bindTransport(String hostname) 
   {
      try {
         return bindTransport(hostname,null,-1);
      } catch (UnknownHostException ex) {
         // Can't happen because the 'address' parameter is null
         return null;
      }
   }
   /**
    * Binds a transport protocol to a specific host name
    * @param hostname The host to bind the transport to
    * @param address The address to restrict the bind to where a null means any
    * @param port The port to restrict the bind to where -1 means any
    */
   protected VirtualHost bindTransport(String hostname,String address,int port)
      throws UnknownHostException
   {
      getLogger().info("Binding "+hostname+"{"+(address==null ? "" : address)+"}:"+port);
      VirtualHost vhost = new VirtualHost(getContext().createChildContext());
      if (hostname!=null && !hostname.equals("*")) {
         vhost.setHostDomain(hostname);
      }
      if (address!=null) {
         InetAddress addr = InetAddress.getByName(address);
         String saddr = addr.toString();
         saddr = saddr.substring(saddr.indexOf('/')+1);
         vhost.setServerAddress(saddr);
      }
      if (port>0) {
         vhost.setHostPort(Integer.toString(port));
      }
      getHosts().add(vhost);
      return vhost;
   }
   
}
